
package at.fhj.ase.crypto.server.net;

import java.io.IOException;
import java.net.Socket;
import java.security.PublicKey;

import org.slf4j.Logger;

import at.fhj.ase.crypto.common.net.MessageHandler;
import at.fhj.ase.crypto.common.util.CommandBuilder;
import at.fhj.ase.crypto.common.util.CryptoUtil;
import at.fhj.ase.crypto.common.util.LogUtil;
import at.fhj.ase.crypto.server.security.SecurityController;

/**
 * Class responsible for all incoming and outgoing messages an {@link ServerClientConnection} (= client socket) takes.
 * <p>
 * There is created one {@link ConnectionHandler} for each {@link ServerClientConnection}, therefore it also hat is as property.<br>
 * It also implements {@link MessageHandler} as it is full responsible for delegating messages that arrive at the socket (which is the
 * {@link ServerClientConnection}).
 * 
 * @author sfe | www.fh-joanneum.at | Practical Aspects of Cryptography *
 */
public class ConnectionHandler implements MessageHandler {
  /** The sole logging instance */
  private final Logger logger = LogUtil.getLogger(this);
  /** The {@link ClientManager} this instance has been created from */
  private final ClientManager clientMgr;
  /** The sole client-server-connection this handler belongs to and is responsible for its handling */
  private final ServerClientConnection srvClntConn;

  /** The public key of the client that is connected with this {@link ConnectionHandler} (one client per instance) */
  private byte[] clientPublicKey;

  /** Save the state of the querying client. If the clicent has not been validated yet, its not allowed to send messages over the network */
  private boolean signatureValidated;

  /**
   * Constructor saving the calling class {@link ClientManager} and creating exactly <strong>one</strong> new {@link ServerClientConnection}.<br>
   * The calling instance is necessary for removing connections ( {@link ClientManager#removeConnection(ServerClientConnection)} ) or broadcasting messages (
   * {@link ClientManager#broadcastToAllClients(String, ServerClientConnection)}).
   * 
   * @param clientSocket the client socket connection we want to create the convenience connection with
   * @param clientMgr the calling instance
   * @throws IOException if there problems instantiating {@link ServerClientConnection}
   */
  public ConnectionHandler(Socket clientSocket, ClientManager clientMgr) throws IOException {
    this.clientMgr = clientMgr;

    logger.debug("Creating new ServerClientConnection associated with this ConnectionHandler...");
    this.srvClntConn = new ServerClientConnection(clientSocket, this);
  }

  /**
   * @see MessageHandler#handleError(String)
   */
  @Override
  public void handleError(String errorMsg) {
    logger.error("handling error '" + errorMsg + "' and removing connection...");
    clientMgr.removeConnection(srvClntConn);
  }

  /**
   * @see MessageHandler#handleMessage(String)
   */
  @Override
  public void handleMessage(String msg) {
    logger.debug("Incoming message < " + msg);

    if (msg.startsWith(CommandBuilder.PUBLIC_KEY)) {
      logger.debug("Retrieved PUBLIC key from client; saving this key and returning server's encrpyted session key...");

      saveClientPublicKey(msg);
      sendEncryptedSessionKey();
    } else if (msg.startsWith(CommandBuilder.SIGNATURE)) {
      logger.debug("Retrieved SIGNATURE key from client, returning whether signature is ok and client is authenticated...");

      signatureValidated = validateSignature(msg);
      if (signatureValidated) {
        sendClientAcknowledge();
      } else {
        sendClientDenial();
        clientMgr.removeConnection(srvClntConn);
        srvClntConn.getSocketWrapper().stop();
      }
    } else if (msg.startsWith(CommandBuilder.DATA)) {
      logger.debug("Normal data is retrieved from a client, broadcasting to all clients...");
      processDataMessage(msg);
    } else {
      logger.error("Retrieved unknown messages, removing client connection...");
      clientMgr.removeConnection(srvClntConn);
    }
  }

  /**
   * Saves the client public key in properties, parsed out of <code>msg</code> which is the arriving client message
   * 
   * @param msg the message that arrives from client containing the public key and the identifying key {@link CommandBuilder#PUBLIC_KEY}
   */
  private void saveClientPublicKey(String msg) {
    final String clientPublicKey = CommandBuilder.parseAndReturnValue(msg, CommandBuilder.PUBLIC_KEY, 1);
    this.clientPublicKey = CryptoUtil.getBytes(clientPublicKey);
  }

  /**
   * Processes an arriving client data message.
   * <p>
   * If the signature for this client has not been validated yet, the connectino is removed from the known client connections.<br>
   * Otherwise this message is broadcasted to all other known clients (chat participants) known by this server.
   */
  private void processDataMessage(String msg) {
    if (!signatureValidated) {
      logger.error("Unknown client is trying to access the network, terminating connection...");
      clientMgr.removeConnection(srvClntConn);
    } else {
      clientMgr.broadcastToAllClients(msg, srvClntConn);
      logger.debug("Successfully sent to all clients");
    }
  }

  /**
   * Sends a denial command using {@link CommandBuilder#DENIED} back to the client.<br>
   * This indicates that the arrived client signature has not been validated by the server.
   */
  private void sendClientDenial() {
    final String msg = new CommandBuilder(CommandBuilder.DENIED).addCommand("Could not be authenticated").toString();
    sendMessage(msg);
  }

  /**
   * Sends an acknowledge to the client using {@link CommandBuilder#AUTHENTICATED} back to the client.<br>
   * This indicates that the arrived client signare has been validated by the server and the client is now allowed to<br>
   * exchange {@link CommandBuilder#DATA} messages (chat messages) with the server and other chat participants.
   */
  private void sendClientAcknowledge() {
    final String msg = new CommandBuilder(CommandBuilder.AUTHENTICATED).addCommand("Authentication successful").toString();
    sendMessage(msg);
  }

  /**
   * Validated an arriving client signature by parsing the message using {@link CommandBuilder} and operating with {@link CryptoUtil}.<br>
   * 
   * @return <code>true</code> if the signed session key (signature) could be validated, otherwise <code>false</code>
   */
  private boolean validateSignature(final String msg) {
    final String signedSessionKey = CommandBuilder.parseAndReturnValue(msg, CommandBuilder.SIGNATURE, 1);
    final byte[] sessionKey = SecurityController.INSTANCE.getSessionKey().getEncoded();
    try {
      final boolean signatureValidated =
        CryptoUtil.verifyWithPublicKey(CryptoUtil.getPublicKey(clientPublicKey), sessionKey, CryptoUtil.getBytes(signedSessionKey));
      logger.debug("Client signature " + (signatureValidated ? "HAS" : "has NOT") + " been validated");
      return signatureValidated;
    } catch (Exception e) {
      logger.error("Could not verify client signature", e);
      return false;
    }
  }

  /**
   * Sends the encrypted message key back to client using {@link CryptoUtil} and {@link CommandBuilder}.
   */
  private void sendEncryptedSessionKey() {
    final String encryptedSessionKeyHex;
    try {
      final PublicKey clientPub = CryptoUtil.getPublicKey(clientPublicKey);
      final byte[] encryptedSessionKey = CryptoUtil.encryptWithPublic(clientPub, SecurityController.INSTANCE.getSessionKey().getEncoded());
      encryptedSessionKeyHex = CryptoUtil.getStringHex(encryptedSessionKey);
    } catch (Exception e) {
      logger.error("Problems creating encrypted session key", e);
      return;
    }

    final String msg = new CommandBuilder(CommandBuilder.SESSION_KEY).addCommand(encryptedSessionKeyHex).toString();
    sendMessage(msg);
  }

  /** @return the {@link ServerClientConnection} that is managed by this {@link ConnectionHandler} */
  final ServerClientConnection getServerClientConnection() {
    return srvClntConn;
  }

  /**
   * Convenience method sending messages back to client using {@link ServerClientConnection#getSocketWrapper()}
   * 
   * @param msg the message that shall be send back to client as {@link String}
   */
  private void sendMessage(String msg) {
    srvClntConn.getSocketWrapper().sendMessage(msg);
  }
}
